/*
 *  linux/tools/build.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * This file builds a disk-image from three different files:
 *
 * - bootsect: max 510 bytes of 8086 machine code, loads the rest
 * - setup: max 4 sectors of 8086 machine code, sets up system parm
 * - system: 80386 code for actual system
 *
 * It does some checking that all files are of the correct type, and
 * just writes the result to stdout, removing headers and padding to
 * the right amount. It also writes some system data to stderr.
 */
/*
* 该程序从三个不同的程序中创建磁盘映像文件：
*
* - bootsect：该文件的8086机器码最长为510字节，用于加载其他程序。
* - setup：该文件的8086机器码最长为4个磁盘扇区，用于设置系统参数。
* - system：实际系统的80386代码。
*
* 该程序首先检查所有程序模块的类型是否正确，并将检查结果在终端上显示出来，
* 然后删除模块头部并扩充大正确的长度。该程序也会将一些系统数据写到stderr。
*/

/*
 * Changes by tytso to allow root device specification
 *
 * Added swap-device specification: Linux 20.12.91
 */
/*
* tytso对该程序作了修改，以允许指定根文件设备。
*
* 添加了指定交换设备功能：Linus 20.12.91
*/

/*
由 MINIX 系统 的编译器 链接器产生的 模块和 可执行文件的头部结构如下所示：
struct exec {
	unsigned char a_magic[2]; 	// 执行文件魔数，必须为0x0301
	unsigned char a_flags; 		// 标志（参见下面说明）
	unsigned char a_cpu; 		// cpu标识号
	unsigned char a_hdrlen; 	// 保留头部长度，32字节或48字节
	unsigned char a_unused; 	// 保留给将来使用
	unsigned short a_version; 	// 版本信息（目前未用）
	long a_text; 				// 代码段长度，字节数
	long a_data; 				// 数据段长度，字节数
	long a_bss; 				// 堆长度，字节数
	long a_entry; 				// 执行入口点地址
	long a_total; 				// 分配的内存总量
	long a_syms; 				// 符号表大小

	// 若头部为32字节，就到此为止
	long a_trsize; 				// 代码段重定位表长度
	long a_drsize; 				// 数据段重定位表长度
	long a_tbase; 				// 代码段重定位基址
	long a_dbase; 				// 数据段重定位基址
};
其中，MINIX执行文件的标志字段a_flags定义为：
	A_UZP 	0x01 				// 未映射的0页（页数）
	A_PAL 	0x02 				// 以页边界调整的可执行文件
	A_NSYM 	0x04 				// 新型符号表
	A_EXEC 	0x10 				// 可执行文件
	A_SEP 	0x20 				// 代码和数据是分开的（I和D独立）

CPU标识号字段a_cpu为：
	A_NONE 		0x00 			// 未知
	A_I8086 	0x04 			// Intel i8086/8088
	A_M68K 		0x0B 			// Motorola m68000
	A_NS16K 	0x0C 			// 国家半导体公司16032
	A_I80386 	0x10 			// Intel i80386
	A_SPARC 	0x17 			// Sun公司SPARC

MINIX 执行头结构 exec 与 Linux 0.12 系统所使用的 a.out 格式执行文件头结构类似。 Linux a.out 格
式执行文件的头部结构及相关信息请参见 linux/include/a.out.h 文件。
*/

#include <stdio.h>				// 使用其中的fprintf()函数
#include <string.h>				// 字符串操作函数
#include <stdlib.h>				// 含exit函数原型说明
#include <sys/types.h>			// 该头文件供unistd.h文件使用
#include <sys/stat.h>			// 含文件状态信息结构定义
#include <linux/fs.h>			// 文件系统头文件
#include <unistd.h>				// 含read/write函数原型说明
#include <fcntl.h>				// 包含文件操作模式符号常数

#define MINIX_HEADER 32			// minix二进制目标文件模块头部长度为32字节
#define GCC_HEADER 1024			// GCC头部信息长度为1024字节

#define SYS_SIZE 0x3000			// system文件最长节数(字节数为SYS_SIZE*16=128KB)

// 默认情况下，Linux根文件系统设备是第2个硬盘的第1个分区（即设备号为0x0306）。这是
// 因为Linus当时开发Linux时，把第1个硬盘用作MINIX系统盘，而第2个硬盘用作为Linux
// 的根文件系统盘。
#define DEFAULT_MAJOR_ROOT 3	// 默认根设备主设备号 - 3（硬盘）
#define DEFAULT_MINOR_ROOT 6	// 默认根设备次设备号 - 6（第2个硬盘的第1分区）

#define DEFAULT_MAJOR_SWAP 0	// 默认交换设备主设备号
#define DEFAULT_MINOR_SWAP 0	// 默认交换设备次设备号

/* max nr of sectors of setup: don't change unless you also change
 * bootsect etc */
/* 下面指定setup模块占的最大扇区数：不要改变该值，除非也改变bootsect等相应文件 */
#define SETUP_SECTS 4			// setup最大长度为4个扇区（2KB）

#define STRINGIFY(x) #x			// 把x转换成字符串类型，用于出错显示语句中

//// 显示出错信息，并终止程序
void die(char * str)
{
	fprintf(stderr,"%s\n",str);
	exit(1);
}

//// 显示程序使用方法，并退出
void usage(void)
{
	die("Usage: build bootsect setup system [rootdev] [> image]");
}

//// 主程序开始。
// 该程序首先检查命令行上的参数是否合规，并设置根设备号和交换设备号，然后分别读取和处理
// bootsect、setup和system模块文件，并写入到已经重定向到Image文件的标准输出中。
// 举例：tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) $(SWAP_DEV) > Image
// argc:6
// argv[0]: tools/build
// argv[1]: boot/bootsect
// argv[2]: boot/setup
// argv[3]: tools/system
// argv[4]: $(ROOT_DEV) 即 ROOT_DEV 的值
// argv[5]: $(SWAP_DEV) 即 SWAP_DEV 的值
int main(int argc, char ** argv)
{
	int i,c,id;
	char buf[1024];
	char major_root, minor_root;
	char major_swap, minor_swap;
	struct stat sb;

	// (1) 首先检查build程序执行时实际命令行参数个数，并根据参数个数作相应设置。
	// build程序要求有4到6个参数，如果命令行上参数个数不满足要求（程序名算作1个），则显
	// 示程序用法后退出执行。
	if ((argc < 4) || (argc > 6))
		usage();

	// 如果程序命令行上有多于4个参数，那么若根设备名不是软盘"FLOPPY"，
	// 则取该设备文件的状态信息，并从中取主、次设备号作为根设备号。若根设备就是FLOPPY设备，
	// 则让主、次设备号取0，表示根设备是当前启动引导设备。
	if (argc > 4) {
		if (strcmp(argv[4], "FLOPPY")) {
			if (stat(argv[4], &sb)) {
				perror(argv[4]);
				die("Couldn't stat root device.");
			}
			major_root = MAJOR(sb.st_rdev);
			minor_root = MINOR(sb.st_rdev);
		} else {
			major_root = 0;
			minor_root = 0;
		}
	} else {
		// 若参数只有4个，则让主设备号和次设备号等于系统默认的根设备号
		major_root = DEFAULT_MAJOR_ROOT;
		minor_root = DEFAULT_MINOR_ROOT;
	}

	// 若程序命令行上有6个参数，那么如果最后一个表示交换设备的参数不是无（"NONE"），则取该
	// 设备文件的状态信息，则从中取主次设备号作为交换设备号。如果最后一个参数就是"NONE"，则
	// 让交换设备的主设备号和次设备号取为0。表示交换设备就是当前启动引导设备。
	if (argc == 6) {
		if (strcmp(argv[5], "NONE")) {
			if (stat(argv[5], &sb)) {
				perror(argv[5]);
				die("Couldn't stat root device.");
			}
			// 取设备名状态结构中设备号
			major_swap = MAJOR(sb.st_rdev);
			minor_swap = MINOR(sb.st_rdev);
		} else {
			major_swap = 0;
			minor_swap = 0;
		}
	} else {
		// 若参数没有6个而是5个，表示命令行上没有带交换设备名。于是就让交换设备主设备号和次设备
		// 号等于系统默认的交换设备号。
		major_swap = DEFAULT_MAJOR_SWAP;
		minor_swap = DEFAULT_MINOR_SWAP;
	}

	// 接下来在标准错误终端上显示上面所选择的根设备主、次设备号和交换设备主、次设备号。如果
	// 主设备号不等于2（软盘）或3（硬盘），也不为0（取系统默认设备），则显示出错信息并退出。
	// 终端的标准输出被定向到文件Image，因此被用于输出保存内核代码数据，生成内核映像文件。
	fprintf(stderr, "Root device is (%d, %d)\n", major_root, minor_root);
	fprintf(stderr, "Swap device is (%d, %d)\n", major_swap, minor_swap);
	if ((major_root != 2) && (major_root != 3) &&
	    (major_root != 0)) {
		fprintf(stderr, "Illegal root device (major = %d)\n",
			major_root);
		die("Bad root device --- major #");
	}
	if (major_swap && major_swap != 3) {
		fprintf(stderr, "Illegal swap device (major = %d)\n",
			major_swap);
		die("Bad root device --- major #");
	}

	// (2) 下面开始执行读取各文件内容并进行相应复制处理。首先初始化1KB的缓冲区，然后以只读
	// 方式打开参数1指定的文件（bootsect），从中读取32字节的MINIX执行文件头结构内容到缓冲区buf中
	for (i=0;i<sizeof buf; i++) buf[i]=0;
	if ((id=open(argv[1],O_RDONLY,0))<0)
		die("Unable to open 'boot'");
	if (read(id,buf,MINIX_HEADER) != MINIX_HEADER)
		die("Unable to read header of 'boot'");

	// 接下来根据MINIX头部结构判断bootsect是否为一个有效的MINIX执行文件。若是，则从文件中
	// 读取512字节的引导扇区代码和数据。其中数值“0x04100301”含义为：0x0301 - MINIX头部的
	// 魔数字段a_magic；0x10 - 可执行标志a_flag；0x04 - 机器类型a_cpu, Intel 8086机器码。
	// 随后对首部信息进行一系列检查。检查头部长度字段a_hdrlen（字节）是否正确（32字节）。
	// （后三字节正好没有用，是0）；确认数据段长a_data字段(long)内容是否为0；确认堆a_bss
	// 字段(long)内容是否为0；确认执行点a_entry字段(long)内容是否为0；确认符号表长字段
	// a_sym的内容是否为0。
	if (((long *) buf)[0]!=0x04100301)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[1]!=MINIX_HEADER)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[3]!=0)
		die("Illegal data segment in 'boot'");
	if (((long *) buf)[4]!=0)
		die("Illegal bss in 'boot'");
	if (((long *) buf)[5] != 0)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[7] != 0)
		die("Illegal symbol table in 'boot'");
	
	// 在上述判断都正确的条件下读取文件中随后的实际代码数据，应该返回读取字节数为512字节。
	// 因为bootsect文件中包含的是1个扇区的引导扇区代码和数据，并且最后2字节应该是可引导
	// 标志0xAA55。
	i=read(id,buf,sizeof buf);
	fprintf(stderr,"Boot sector %d bytes.\n",i);
	if (i != 512)
		die("Boot block must be exactly 512 bytes");
	if ((*(unsigned short *)(buf+510)) != 0xAA55)
		die("Boot block hasn't got boot flag (0xAA55)");

	// 然后修改缓冲中内容，506、507偏移处需存放交换设备号，508、509偏移处需存放根设备号
	buf[506] = (char) minor_swap;
	buf[507] = (char) major_swap;
	buf[508] = (char) minor_root;
	buf[509] = (char) major_root;	

	// 接下来，将该512字节的数据写到标准输出stdout，并关闭bootsect文件。在linux/Makefile
	// 中， build程序利用”>”指示符把标准输出重定向到内核映像文件Image上，因此引导扇区代码
	// 和数据会被写到Image开始的512字节处。
	i=write(1,buf,512);
	if (i!=512)
		die("Write call failed");
	close (id);
	
	// (3) 下面首先以只读方式打开参数2指定的文件（setup），并从中读取32字节的MINIX执行文
	// 件头结构内容到缓冲区buf中。接下来根据MINIX头部结构判断setup是否为一个有效的MINIX
	// 执行文件。若是，则对首部信息进行一系列的检查操作，处理方式与上面相同。
	if ((id=open(argv[2],O_RDONLY,0))<0)
		die("Unable to open 'setup'");
	if (read(id,buf,MINIX_HEADER) != MINIX_HEADER)
		die("Unable to read header of 'setup'");
	if (((long *) buf)[0]!=0x04100301)
		die("Non-Minix header of 'setup'");
	if (((long *) buf)[1]!=MINIX_HEADER)
		die("Non-Minix header of 'setup'");
	if (((long *) buf)[3]!=0)		// 数据段长a_data字段
		die("Illegal data segment in 'setup'");
	if (((long *) buf)[4]!=0)		// 堆a_bss字段
		die("Illegal bss in 'setup'");
	if (((long *) buf)[5] != 0)		// 执行起始点a_entry字段
		die("Non-Minix header of 'setup'");
	if (((long *) buf)[7] != 0)
		die("Illegal symbol table in 'setup'");

	// 在上述判断都正确的条件下读取文件中随后的实际代码数据，并且写到终端标准输出。同时统计
	// 写的长度（i），并在操作结束后关闭setup文件。之后判断执行写操作的代码和数据长度值，该
	// 值不应大于(SETUP_SECTS * 512)字节，否则就得重新修改build、bootsect和setup程序中设定
	// 的setup所占扇区数并重新编译内核。若一切正常就显示setup实际长度值。
	for (i=0 ; (c=read(id,buf,sizeof buf))>0 ; i+=c )
		if (write(1,buf,c)!=c)
			die("Write call failed");
	close (id);
	if (i > SETUP_SECTS*512)
		die("Setup exceeds " STRINGIFY(SETUP_SECTS)
			" sectors - rewrite build/boot/setup");
	fprintf(stderr,"Setup is %d bytes.\n",i);

	// 在将缓冲区buf清零之后，判断实际写的setup长度与(SETUP_SECTS * 512)的数值差，若setup
	// 长度小于该长度（4 * 512字节），则用NULL字符将setup填足为4 * 512字节。
	for (c=0 ; c<sizeof(buf) ; c++)
		buf[c] = '\0';
	while (i<SETUP_SECTS*512) {
		c = SETUP_SECTS*512-i;
		if (c > sizeof(buf))
			c = sizeof(buf);
		if (write(1,buf,c) != c)
			die("Write call failed");
		i += c;
	}
	
	// (4) 下面开始处理system模块文件。该文件使用gas/gcc编译，因此具有a.out目标文件格式。
	// 首先以只读方式打开文件，并读取其中a.out格式头部结构信息（1KB长度）。在判断system
	// 是一个有效的a.out格式文件之后，就把该文件随后的所有数据都写到标准输出（Image文件）
	// 中，并关闭该文件。然后显示system模块的长度。若system代码和数据长度超过SYS_SIZE节
	// （即128KB字节），则显示出错信息并退出。若无错，则返回0，表示正常退出。
	if ((id=open(argv[3],O_RDONLY,0))<0)
		die("Unable to open 'system'");
	if (read(id,buf,GCC_HEADER) != GCC_HEADER)
		die("Unable to read header of 'system'");
	if (((long *) buf)[5] != 0)		// 执行入口点字段a_entry值应为0
		die("Non-GCC header of 'system'");
	for (i=0 ; (c=read(id,buf,sizeof buf))>0 ; i+=c )
		if (write(1,buf,c)!=c)
			die("Write call failed");
	close(id);
	fprintf(stderr,"System is %d bytes.\n",i);
	if (i > SYS_SIZE*16)
		die("System is too big");
	return(0);
}
